{
 "cells": [
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-09-17T16:25:53.687768Z",
     "start_time": "2025-09-17T16:25:53.664755Z"
    }
   },
   "cell_type": "code",
   "source": [
    "import cv2\n",
    "\n",
    "def sort_contours(cnts, method=\"left-to-right\"):\n",
    "    reverse = False\n",
    "    i = 0\n",
    "\n",
    "    if method == \"right-to-left\" or method == \"bottom-to-top\":\n",
    "        reverse = True\n",
    "\n",
    "    if method == \"top-to-bottom\" or method == \"bottom-to-top\":\n",
    "        i = 1\n",
    "    boundingBoxes = [cv2.boundingRect(c) for c in cnts] #用一个最小的矩形，把找到的形状包起来x,y,h,w\n",
    "    (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),\n",
    "                                        key=lambda b: b[1][i], reverse=reverse))\n",
    "\n",
    "    return cnts, boundingBoxes\n",
    "def resize(image, width=None, height=None, inter=cv2.INTER_AREA):\n",
    "    dim = None\n",
    "    (h, w) = image.shape[:2]\n",
    "    if width is None and height is None:\n",
    "        return image\n",
    "    if width is None:\n",
    "        r = height / float(h)\n",
    "        dim = (int(w * r), height)\n",
    "    else:\n",
    "        r = width / float(w)\n",
    "        dim = (width, int(h * r))\n",
    "    resized = cv2.resize(image, dim, interpolation=inter)\n",
    "    return resized"
   ],
   "id": "d5d7ff0a1a8ceb64",
   "outputs": [],
   "execution_count": 2
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-09-17T16:26:47.802600Z",
     "start_time": "2025-09-17T16:26:42.154814Z"
    }
   },
   "cell_type": "code",
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import cv2\n",
    "\n",
    "def cv_show(img, title='Image'):\n",
    "    cv2.imshow(title, img)\n",
    "    cv2.waitKey(0)\n",
    "    cv2.destroyAllWindows()\n",
    "\n",
    "# 处理模版\n",
    "template = cv2.imread(r'data/ocr_a_reference.png', 0)\n",
    "template = cv2.threshold(template, 10, 255, cv2.THRESH_BINARY_INV)[1]\n",
    "# cv_show(template)\n",
    "template_contours, _ = cv2.findContours(template.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n",
    "template_contours = sort_contours(template_contours, method=\"left-to-right\")[0]\n",
    "template_color = cv2.cvtColor(template, cv2.COLOR_GRAY2BGR)\n",
    "# cv2.drawContours(template_color, template_contours, -1, (0, 0, 255), 2)\n",
    "# cv_show(template_color)\n",
    "\n",
    "# 存储数字的ROI\n",
    "digits = {}\n",
    "for i, c in enumerate(template_contours):\n",
    "    (x, y, w, h) = cv2.boundingRect(c)\n",
    "    # cv2.rectangle(template_color, (x, y), (x + w, y + h), (0, 255, 0), 2)\n",
    "    roi = template[y:y + h, x:x + w]\n",
    "    roi = cv2.resize(roi, (57, 88))\n",
    "    digits[i] = roi\n",
    "# print(digits)\n",
    "# cv2.imshow(\"template_color\", template_color)\n",
    "\n",
    "# 定义信用卡类型映射\n",
    "FIRST_NUMBER = {\n",
    "    \"3\": \"American Express\",\n",
    "    \"4\": \"Visa\",\n",
    "    \"5\": \"MasterCard\",\n",
    "    \"6\": \"Discover Card\"\n",
    "}\n",
    "\n",
    "# 初始化卷积核\n",
    "rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))\n",
    "sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))\n",
    "\n",
    "imgs = []\n",
    "imgs_color = []\n",
    "all_contours = []\n",
    "all_result = []\n",
    "\n",
    "for i in range(5):\n",
    "    img_color = cv2.imread(f'案例一/data/credit_card_0{i + 1}.png')\n",
    "    img_color = resize(img_color, width=300)\n",
    "    img_gray = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY)\n",
    "    imgs.append(img_gray)\n",
    "    imgs_color.append(img_color)\n",
    "    cv_show(img_color)\n",
    "    # cv_show(img_gray)\n",
    "\n",
    "    # 礼帽操作，突出明亮区域\n",
    "    tophat = cv2.morphologyEx(img_gray, cv2.MORPH_TOPHAT, rectKernel)\n",
    "    # cv_show(tophat)\n",
    "\n",
    "    gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1)\n",
    "    gradX = np.absolute(gradX)\n",
    "    (minVal, maxVal) = (np.min(gradX), np.max(gradX))\n",
    "    gradX = (255 * ((gradX - minVal) / (maxVal - minVal)))\n",
    "    gradX = gradX.astype(\"uint8\")\n",
    "\n",
    "    # print(np.array(gradX).shape)\n",
    "    # cv_show(gradX) # 梯度\n",
    "\n",
    "    # 闭操作，填充数字之间的空隙\n",
    "    gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel)\n",
    "    # cv_show(gradX)\n",
    "    thresh = cv2.threshold(gradX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]\n",
    "    # cv_show(thresh) # 二值化\n",
    "\n",
    "    # 再来一个闭操作\n",
    "    thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)\n",
    "    # cv_show(thresh) # 第二次闭操作\n",
    "\n",
    "    contour, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n",
    "    all_contours.append(contour)\n",
    "    cv2.drawContours(img_color, contour, -1, (0, 0, 255), 2)\n",
    "    # cv_show(img_color) # 轮廓\n",
    "\n",
    "    locs = []\n",
    "    # 遍历轮廓\n",
    "    for (j, c) in enumerate(contour):\n",
    "        # 计算矩形\n",
    "        (x, y, w, h) = cv2.boundingRect(c)\n",
    "        ar = w / float(h)\n",
    "        if 2.5 < ar < 4.0:\n",
    "            if (w > 40 and w < 55) and (h > 10 and h < 40):\n",
    "                locs.append((x, y, w, h))\n",
    "\n",
    "    # 将符合的轮廓从左向右排列\n",
    "    locs = sorted(locs, key=lambda x: x[0])\n",
    "    cv2.drawContours(img_color, contour, -1, (0, 0, 255), 2)\n",
    "    # cv_show(img_color) # 符合条件的轮廓\n",
    "\n",
    "    output = []\n",
    "    for (k, (gX, gY, gW, gH)) in enumerate(locs):\n",
    "        groupOutput = []\n",
    "        group = img_gray[gY - 5:gY + gH + 5, gX - 5:gX + gW + 5]\n",
    "        # cv_show(group)\n",
    "        # 预处理\n",
    "        group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]\n",
    "        # cv_show(group)\n",
    "        digitCnt, hierarchy = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n",
    "        digitCnt = sort_contours(digitCnt, method=\"left-to-right\")[0]\n",
    "\n",
    "        # 计算每一组中的每一个数据\n",
    "        for c in digitCnt:\n",
    "            (x, y, w, h) = cv2.boundingRect(c)\n",
    "            roi = group[y:y + h, x:x + w]\n",
    "            roi = cv2.resize(roi, (57, 88))\n",
    "            # cv_show(roi)\n",
    "\n",
    "            scores = []\n",
    "            for (digit, digit_template) in digits.items():\n",
    "                result = cv2.matchTemplate(roi, digit_template, cv2.TM_CCOEFF)\n",
    "                _, score, _, _ = cv2.minMaxLoc(result)\n",
    "                scores.append(score)\n",
    "\n",
    "            groupOutput.append(str(np.argmax(scores)))\n",
    "\n",
    "        cv2.rectangle(img_color, (gX-5, gY-5), (gX + gW+5, gY + gH+5), (0, 0, 255), 1)\n",
    "        cv2.putText(img_color, \"\".join(groupOutput), (gX, gY - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)\n",
    "        output.extend(groupOutput)\n",
    "\n",
    "    card_type = FIRST_NUMBER.get(output[0], 'Unknown')\n",
    "    card_number = ''.join(output)\n",
    "    all_result.append((i+1, card_type, card_number, img_color))\n",
    "\n",
    "    cv2.imshow(\"Credit Card\", img_color)\n",
    "    cv2.waitKey(0)\n",
    "    cv2.destroyAllWindows()\n",
    "print(\"\\n=== 信用卡识别结果 ===\")\n",
    "for i, card_type, card_number, img_color in all_result:\n",
    "    print(f\"信用卡 {i} 类型: {card_type}\")\n",
    "    print(f\"信用卡 {i} 号码: {card_number}\")"
   ],
   "id": "44b3225fa696277a",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== 信用卡识别结果 ===\n",
      "信用卡 1 类型: Visa\n",
      "信用卡 1 号码: 4000123456789010\n",
      "信用卡 2 类型: Visa\n",
      "信用卡 2 号码: 4020340002345678\n",
      "信用卡 3 类型: MasterCard\n",
      "信用卡 3 号码: 5412751234567890\n",
      "信用卡 4 类型: Visa\n",
      "信用卡 4 号码: 4000123456789010\n",
      "信用卡 5 类型: MasterCard\n",
      "信用卡 5 号码: 5476767898765432\n"
     ]
    }
   ],
   "execution_count": 4
  },
  {
   "metadata": {
    "collapsed": true,
    "ExecuteTime": {
     "end_time": "2025-09-17T14:16:16.604604Z",
     "start_time": "2025-09-17T14:13:23.064190Z"
    }
   },
   "cell_type": "code",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "信用卡 1 类型: Discover Card\n",
      "信用卡 1 号码: 622\n",
      "信用卡 2 类型: Visa\n",
      "信用卡 2 号码: 42222\n",
      "信用卡 3 未识别到有效数字\n",
      "信用卡 4 未识别到有效数字\n",
      "信用卡 5 未识别到有效数字\n"
     ]
    }
   ],
   "execution_count": 4,
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import cv2\n",
    "\n",
    "def cv_show(img, title='Image'):\n",
    "    cv2.imshow(title, img)\n",
    "    cv2.waitKey(0)\n",
    "    cv2.destroyAllWindows()\n",
    "\n",
    "# 处理模版\n",
    "template = cv2.imread(r'data/ocr_a_reference.png', 0)\n",
    "template = cv2.threshold(template, 10, 255, cv2.THRESH_BINARY)[1]\n",
    "# cv_show(template)\n",
    "template_contours, _ = cv2.findContours(template.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)\n",
    "template_contours = sort_contours(template_contours, method=\"left-to-right\")[0]\n",
    "# template_color = cv2.cvtColor(template, cv2.COLOR_GRAY2BGR)\n",
    "# cv2.drawContours(template_color, template_contours, -1, (0, 0, 255), 2)\n",
    "# cv_show(template_color)\n",
    "\n",
    "# 存储数字的ROI\n",
    "digits = {}\n",
    "for i, c in enumerate(template_contours):\n",
    "    (x, y, w, h) = cv2.boundingRect(c)\n",
    "    roi = template[y:y + h, x:x + w]\n",
    "    roi = cv2.resize(roi, (50, 80))\n",
    "    digits[i] = roi\n",
    "# print(digits)\n",
    "\n",
    "# 定义信用卡类型映射\n",
    "FIRST_NUMBER = {\n",
    "    \"3\": \"American Express\",\n",
    "    \"4\": \"Visa\",\n",
    "    \"5\": \"MasterCard\",\n",
    "    \"6\": \"Discover Card\"\n",
    "}\n",
    "\n",
    "imgs = []\n",
    "all_contours = []\n",
    "\n",
    "for i in range(5):\n",
    "    imgs.append(cv2.imread(f'案例一/data/credit_card_0{i + 1}.png', 0))\n",
    "    # 二值化\n",
    "    imgs[i] = cv2.threshold(imgs[i], 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]\n",
    "    # 计算轮廓\n",
    "    contours, hierarchy = cv2.findContours(imgs[i], cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)\n",
    "    all_contours.append(contours)\n",
    "\n",
    "cv_show(imgs[0])\n",
    "\n",
    "for idx, (img, cnts) in enumerate(zip(imgs, all_contours)):\n",
    "    img_color = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)\n",
    "    locs = []\n",
    "\n",
    "    for c in cnts:\n",
    "        (x, y, w, h) = cv2.boundingRect(c)\n",
    "        # 计算宽高比，筛选符合信用卡数字特征的区域（4个数字一组）\n",
    "        ar = w / float(h)\n",
    "        if 2.5 < ar < 4.0 and 30 < w < 60 and 10 < h < 20:\n",
    "            locs.append((x, y, w, h))\n",
    "\n",
    "    locs = sorted(locs, key=lambda x: x[0])\n",
    "    output = []\n",
    "\n",
    "    # 识别每个数字区域\n",
    "    for(gX, gY, gW, gH) in locs:\n",
    "        group = img[gY - 2:gY + gH + 2, gX - 2:gX + gW + 2]\n",
    "        group_cnts, _ = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n",
    "        group_cnts = sort_contours(group_cnts, method=\"left-to-right\")[0]\n",
    "\n",
    "        group_output = []\n",
    "        for c in group_cnts:\n",
    "            (x, y, w, h) = cv2.boundingRect(c)\n",
    "            digits_roi = group[y:y + h, x:x + w]\n",
    "            digits_roi = cv2.resize(digits_roi, (50, 80))\n",
    "\n",
    "            # 模糊匹配\n",
    "            scores = []\n",
    "            for (digit, digit_template) in digits.items():\n",
    "                result = cv2.matchTemplate(digits_roi, digit_template, cv2.TM_CCOEFF)\n",
    "                _, score, _, _ = cv2.minMaxLoc(result)\n",
    "                scores.append(score)\n",
    "\n",
    "            group_output.append(str(np.argmax(scores)))\n",
    "\n",
    "        cv2.rectangle(img_color, (gX-2, gY-2), (gX + gW+2, gY + gH+2), (0, 255, 0), 1)\n",
    "        cv2.putText(img_color, \"\".join(group_output), (gX, gY - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 255, 0), 2)\n",
    "        output.extend(group_output)\n",
    "\n",
    "    # 输出识别结果\n",
    "    if output:\n",
    "        print(f\"信用卡 {idx+1} 类型: {FIRST_NUMBER.get(output[0], 'Unknown')}\")\n",
    "        print(f\"信用卡 {idx+1} 号码: {''.join(output)}\")\n",
    "    else:\n",
    "        print(f\"信用卡 {idx+1} 未识别到有效数字\")\n",
    "\n",
    "    # 显示结果图像\n",
    "    cv_show(img_color, f\"Credit Card {idx+1} Result\")"
   ],
   "id": "initial_id"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": "",
   "id": "81794e6191c2ecaf"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
